package fr.vahren.dotastrat.game;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.json.simple.JSONArray;
import org.json.simple.JSONObject;

import fr.vahren.dotastrat.game.command.D2SAttackCommand;
import fr.vahren.dotastrat.game.command.D2SCommand;
import fr.vahren.dotastrat.game.command.D2SCreepAction;
import fr.vahren.dotastrat.game.command.D2SMoveCommande;
import fr.vahren.dotastrat.game.effect.D2SEffect;
import fr.vahren.dotastrat.model.D2SAbility;
import fr.vahren.dotastrat.model.D2SCreepType;
import fr.vahren.dotastrat.model.D2SCreeps;
import fr.vahren.dotastrat.model.D2SHeroUnit;
import fr.vahren.dotastrat.model.D2SItem;
import fr.vahren.dotastrat.model.D2SMap;
import fr.vahren.dotastrat.model.D2SPlayer;
import fr.vahren.dotastrat.model.D2SSide;
import fr.vahren.dotastrat.model.D2STower;
import fr.vahren.dotastrat.model.D2SUnit;
import fr.vahren.dotastrat.model.D2SWards;
import fr.vahren.dotastrat.model.D2SZone;
import fr.vahren.dotastrat.utils.D2SJSONUtils;

/**
 * @author fdroumag
 * 
 */
public class D2SGame {

	private static final int NB_CREEP_PER_SPAWN = 3;

	private static final int CREEP_SPAWN_FREQ = 4;

	private static int idCount = 0;

	private static final String DESTINATION = "destination";

	private static final String HERO = "hero";

	private static final String ACTION_TYPE = "actionType";

	private static final String ABILITY_ID = "abilityId";

	private static final String ITEM_ID = "itemId";

	private static final String TARGET_ZONE = "targetZone";

	private static final String TARGET = "target";

	private static final String ABILITY = "ability";

	private static final String ITEM = "item";

	private static final String ATTACK = "attack";

	private static final String MOVE = "move";

	private static final int[] zoneWithTowers = { 2, 4, 6, 7, 15, 39, 45, 50,
			13, 12, 26, 31, 38, 46, 51, 53, 55, 36, 28, 14 };

	private static final int[] direMinionSpawn = { 6, 13, 14 };
	private static final int[] radiantMinionSpawn = { 45, 46, 51 };

	private static Map<Integer, Integer> direCreepMovementMap = new HashMap<>();
	private static Map<Integer, Integer> radiantCreepMovementMap = new HashMap<>();

	static {
		// TOP
		direCreepMovementMap.put(6, 5);
		direCreepMovementMap.put(5, 4);
		direCreepMovementMap.put(4, 3);
		direCreepMovementMap.put(3, 2);
		direCreepMovementMap.put(2, 1);
		direCreepMovementMap.put(1, 15);
		direCreepMovementMap.put(15, 22);
		direCreepMovementMap.put(22, 29);
		direCreepMovementMap.put(29, 37);
		direCreepMovementMap.put(37, 45);
		direCreepMovementMap.put(45, 50);

		radiantCreepMovementMap.put(45, 37);
		radiantCreepMovementMap.put(37, 29);
		radiantCreepMovementMap.put(29, 22);
		radiantCreepMovementMap.put(22, 15);
		radiantCreepMovementMap.put(15, 1);
		radiantCreepMovementMap.put(1, 2);
		radiantCreepMovementMap.put(2, 3);
		radiantCreepMovementMap.put(3, 4);
		radiantCreepMovementMap.put(4, 5);
		radiantCreepMovementMap.put(5, 6);
		radiantCreepMovementMap.put(6, 7);

		// MID
		direCreepMovementMap.put(13, 12);
		direCreepMovementMap.put(12, 19);
		direCreepMovementMap.put(19, 24);
		direCreepMovementMap.put(24, 32);
		direCreepMovementMap.put(32, 31);
		direCreepMovementMap.put(31, 39);
		direCreepMovementMap.put(39, 38);
		direCreepMovementMap.put(38, 46);
		direCreepMovementMap.put(46, 50);

		radiantCreepMovementMap.put(46, 38);
		radiantCreepMovementMap.put(38, 39);
		radiantCreepMovementMap.put(39, 31);
		radiantCreepMovementMap.put(31, 32);
		radiantCreepMovementMap.put(32, 24);
		radiantCreepMovementMap.put(24, 19);
		radiantCreepMovementMap.put(19, 12);
		radiantCreepMovementMap.put(12, 13);
		radiantCreepMovementMap.put(13, 7);

		// BOT
		direCreepMovementMap.put(14, 21);
		direCreepMovementMap.put(21, 28);
		direCreepMovementMap.put(28, 36);
		direCreepMovementMap.put(36, 44);
		direCreepMovementMap.put(44, 55);
		direCreepMovementMap.put(55, 54);
		direCreepMovementMap.put(54, 53);
		direCreepMovementMap.put(53, 52);
		direCreepMovementMap.put(52, 51);
		direCreepMovementMap.put(51, 50);

		radiantCreepMovementMap.put(51, 52);
		radiantCreepMovementMap.put(52, 53);
		radiantCreepMovementMap.put(53, 54);
		radiantCreepMovementMap.put(54, 55);
		radiantCreepMovementMap.put(55, 44);
		radiantCreepMovementMap.put(44, 36);
		radiantCreepMovementMap.put(36, 28);
		radiantCreepMovementMap.put(28, 21);
		radiantCreepMovementMap.put(21, 14);
		radiantCreepMovementMap.put(14, 7);
	}

	private int id;

	private List<D2SPlayer> players = new ArrayList<>();

	private D2SMap map;

	private D2SGameState phase;

	private D2SActionStack actionStack = new D2SActionStack();

	private Map<Integer, List<D2SMessage>> playerMessages = new HashMap<>();

	private int turn;

	public void startNewGame(Map<D2SPlayer, List<D2SHeroUnit>> heroSelection,
			int id) {

		this.id = id;
		// init phase
		phase = D2SGameState.INIT;

		// add heroes
		for (D2SPlayer player : heroSelection.keySet()) {
			players.add(player);
			playerMessages.put(player.getId(), new ArrayList<D2SMessage>());
			sendMessageTo(player.getId(), "START", D2SMessageType.GENERAL);
			for (D2SHeroUnit hero : heroSelection.get(player)) {
				player.addControlledUnit(hero);
			}
		}

		// create a new map
		map = new D2SMap(this);

		// place heroes at spawn
		for (D2SPlayer player : players) {
			map.putHeroesAtSpawn(player.getControlledUnits(), player.getSide());
		}

		// init phase finished

	}

	public JSONObject info(JSONObject request) {
		int playerId = Integer.parseInt((String) request.get("playerId"));
		D2SPlayer player = getPlayer(playerId);
		return generateGameInfo(player.getSide(), playerId);
	}

	public JSONObject act(JSONObject request) {

		int playerId = Integer.parseInt((String) request.get("playerId"));
		D2SPlayer player = getPlayer(playerId);
		if (phase != D2SGameState.CHOOSE_ACTION) {
			System.err.println("Error not in ACT phase");
		} else {

			JSONObject action = (JSONObject) request.get("chosenAction");
			createAction(player, action);

			if (haveAllUnitsChosenAction()) {
				resolve();
			}
		}
		return generateGameInfo(player.getSide(), playerId);

	}

	private void createAction(D2SPlayer player, JSONObject action) {
		String actionType = (String) action.get(ACTION_TYPE);
		int heroId = Integer.parseInt((String) action.get(HERO));
		D2SHeroUnit hero = player.getUnit(heroId);
		if (MOVE.equals(actionType)) {
			createMove(player, action, hero);
		} else if (ATTACK.equals(actionType)) {
			createAttack(player, action, hero);

		} else if (ITEM.equals(actionType) || ABILITY.equals(actionType)) {
			createUse(player, action, actionType, hero);
		}
		if ((boolean) action.get("final")) {
			hero.setHasChosenAction(true);
		}

	}

	private void createUse(D2SPlayer player, JSONObject action,
			String actionType, D2SHeroUnit hero) {
		D2SZone targetZone = null;
		List<D2SUnit> targets = null;
		if (action.get(TARGET) != null) {
			targets = new ArrayList<>();
			JSONArray jsonTargets = (JSONArray) action.get(TARGET);
			for (Object t : jsonTargets) {
				targets.add(getUnit(Integer.parseInt((String) t)));
			}
		}
		if (action.get(TARGET_ZONE) != null) {
			targetZone = map.getZone(Integer.parseInt((String) action
					.get(TARGET_ZONE)));
		}
		if (ITEM.equals(actionType)) {
			D2SItem item = hero.getItem(Integer.parseInt((String) action
					.get(ITEM_ID)));
			actionStack.add(new D2SCommand(item, player, hero, map, targets,
					targetZone, this));
		} else {
			D2SAbility ability = hero.getAbility(Integer
					.parseInt((String) action.get(ABILITY_ID)));
			actionStack.add(new D2SCommand(ability, player, hero, map, targets,
					targetZone, this));
		}
	}

	private void createAttack(D2SPlayer player, JSONObject action,
			D2SHeroUnit hero) {
		D2SUnit targetUnit = getUnit(Integer.parseInt((String) action
				.get(TARGET)));
		List<D2SUnit> targets = new ArrayList<>();
		targets.add(targetUnit);
		actionStack.add(new D2SAttackCommand(player, map, hero, targets));
	}

	private void createMove(D2SPlayer player, JSONObject action,
			D2SHeroUnit hero) {
		int zoneId = Integer.parseInt((String) action.get(DESTINATION));
		D2SZone dest = map.getZone(zoneId);
		actionStack.add(new D2SMoveCommande(player, map, hero, dest, this));
		hero.setMoving(true);
	}

	public D2SUnit getUnit(int id) {
		for (D2SPlayer player : players) {
			D2SHeroUnit unit = player.getUnit(id);
			if (unit != null) {
				return unit;
			}
		}
		// search in towers and barracks and ancient
		for (D2SZone zone : map.getZones()) {
			for (D2STower t : zone.getTowers()) {
				if (t.getId() == id) {
					return t;
				}
			}
			if (zone.getRangedBarracks() != null
					&& zone.getRangedBarracks().getId() == id) {
				return zone.getRangedBarracks();
			}
			if (zone.getMeleeBarracks() != null
					&& zone.getMeleeBarracks().getId() == id) {
				return zone.getMeleeBarracks();
			}
		}

		if (map.getRadiantAncient() != null
				&& map.getRadiantAncient().getId() == id) {
			return map.getRadiantAncient();
		}
		if (map.getDireAncient() != null && map.getDireAncient().getId() == id) {
			return map.getDireAncient();
		}

		return null;
	}

	private void resolve() {
		// this is the part which is time consuming so start a separate thread
		Thread t = new Thread(new Runnable() {

			@Override
			public void run() {
				D2SGame.this.resolveActionStack();
			}
		});
		t.start();
	}

	protected void resolveActionStack() {
		phase = D2SGameState.RESOLVE;
		// resolve every action by order of priority
		for (int i = 4; i >= 0; i--) {
			System.out.println("executing " + actionStack.getActions(i).size()
					+ " actions of priority " + i);
			for (D2SCommand c : actionStack.getActions(i)) {
				c.execute();
			}
			if (!actionStack.getActions(i).isEmpty()) {
				// check deaths of heroUnits
				for (D2SPlayer player : players) {
					for (D2SHeroUnit unit : player.getControlledUnits()) {
						handleDeath(unit);
					}
				}
				// check deaths of other units
				// creeps and buildings
				for (D2SZone zone : map.getZones()) {
					for (D2SCreeps c : zone.getCreeps()) {
						handleDeath(c);
					}
					for (D2STower t : zone.getTowers()) {
						if (t.isDead()) {
							handleTowerDeath(zone, t);
						}
					}
					if (zone.getRangedBarracks() != null
							&& zone.getRangedBarracks().isDead()) {
						handleTowerDeath(zone, zone.getRangedBarracks());
					}
					if (zone.getMeleeBarracks() != null
							&& zone.getMeleeBarracks().isDead()) {
						handleTowerDeath(zone, zone.getMeleeBarracks());
					}
				}

			}

			// all done
			actionStack.clear(i);

		}
		// tower damage
		for (int zoneId : zoneWithTowers) {
			D2SZone zone = map.getZone(zoneId);
			for (D2STower tower : zone.getTowers()) {
				towerDamage(zone, tower);
			}
		}

		// EFFECTS
		// on zones
		for (D2SZone zone : map.getZones()) {
			for (D2SEffect effect : zone.getEffects()) {
				applyEffectOnZone(zone, effect);
			}
		}

		// on units (which are not dead at the end of the turn)
		// they can still die though
		for (D2SPlayer player : players) {
			for (D2SHeroUnit unit : player.getControlledUnits()) {
				for (D2SEffect effect : unit.getEffects()) {
					applyEffectOnUnit(unit, effect);
				}
				handleDeath(unit);
			}
		}

		// Done, next phase
		phase = D2SGameState.ENDING;
	}

	private void handleDeath(D2SCreeps c) {
		if (c.isDead()) {
			giveBounty(c, c.getAttackers());
			removeCreepFromMap(c);
		}
	}

	private void removeCreepFromMap(D2SCreeps c) {
		map.removeCreep(c);
	}

	private void towerDamage(D2SZone zone, D2STower tower) {
		// get target and damage it
		D2SUnit target = getTowerTarget(zone, tower.getSide());
		if (target != null) {
			tower.applyAttackDamage(target, false, tower);
			if (target instanceof D2SHeroUnit) {
				String message = target.getName()
						+ " is attacked by the tower!";
				sendToAnyoneThatCanSee(target.getId(),
						Arrays.asList(zone.getId()), tower.getSide(), message,
						D2SMessageType.ATTACK);
			}
		}
	}

	private void handleTowerDeath(D2SZone zone, D2STower tower) {
		// give bounty and remove tower from zone
		D2SSide s = getOppositeSide(tower.getSide());
		for (D2SPlayer p : players) {
			if (p.getSide() == s) {
				for (D2SHeroUnit unit : p.getControlledUnits()) {
					if (unit.isHero()) {
						unit.earn(tower.getBounty(), true);
					}
				}
			}
		}
		zone.removeTower(tower);
	}

	private D2SSide getOppositeSide(D2SSide side) {
		return side == D2SSide.DIRE ? D2SSide.RADIANT : D2SSide.DIRE;
	}

	private D2SUnit getTowerTarget(D2SZone zone, D2SSide side) {
		for (D2SHeroUnit unit : zone.getUnits()) {
			if (unit.getSide() != side && unit.isHasAttackedHero()) {
				return unit;
			}
		}
		if (!zone.getCreeps().isEmpty()) {
			for (D2SCreeps c : zone.getCreeps()) {
				if (c.getSide() != side) {
					return c;
				}
			}
		}
		if (!zone.getUnits().isEmpty()) {
			for (D2SHeroUnit u : zone.getUnits()) {
				if (u.getSide() != side) {
					return u;
				}
			}
		}
		return null;

	}

	private void handleDeath(D2SHeroUnit unit) {
		if (unit.isDead()) {
			unit.die();
			if (unit.isHero()) {
				sendMessageToEveryone(
						generateKillMessage(unit, unit.getAttackers()),
						D2SMessageType.DEATH);
				giveBounty(unit, unit.getAttackers());

			}
			removeUnitFromMap(unit);
		}
	}

	private void applyEffectOnZone(D2SZone zone, D2SEffect effect) {
		effect.applyEffect();
		effect.tick();
		if (effect.getRemainingTurns() == 0) {
			effect.finish();
			zone.removeEffect(effect);
		}
	}

	private void applyEffectOnUnit(D2SHeroUnit unit, D2SEffect effect) {
		effect.applyEffect();
		effect.tick();
		if (effect.getRemainingTurns() == 0) {
			effect.finish();
			unit.removeEffect(effect);
		}
	}

	/**
	 * Removes unit from map.
	 * 
	 * @param unit
	 */
	private void removeUnitFromMap(D2SHeroUnit unit) {
		map.removeUnit(unit);
	}

	private void sendMessageToEveryone(String message, D2SMessageType type) {
		for (D2SPlayer p : players) {
			sendMessageTo(p.getId(), message, type);
		}
	}

	private String generateKillMessage(D2SHeroUnit unit, List<Integer> attackers) {
		StringBuffer s = new StringBuffer();
		s.append(unit.getName() + " was slain by :");
		for (int a : attackers) {
			s.append(" " + getUnit(a).getName());
		}
		return s.toString();
	}

	/**
	 * Gives bounty in gold and exp to the attackers
	 * 
	 * @param unit
	 * @param attackers
	 */
	private void giveBounty(D2SUnit unit, List<Integer> attackers) {
		// calculate gold bounty
		int gold = unit.getGoldBounty(attackers.size());
		// calculate exp bounty
		int exp = unit.getExpBounty(attackers.size());

		for (int att : attackers) {
			((D2SHeroUnit) getUnit(att)).exp(exp);
			((D2SHeroUnit) getUnit(att)).earn(gold, true);
		}
	}

	private boolean haveAllUnitsChosenAction() {
		for (D2SPlayer player : players) {
			for (D2SHeroUnit unit : player.getControlledUnits()) {
				if (!unit.isHasChosenAction()) {
					return false;
				}
			}
		}
		return true;
	}

	private boolean haveAllPlayersEnded() {
		for (D2SPlayer player : players) {
			if (!player.isHasEnded()) {
				return false;
			}
		}
		return true;
	}

	public JSONObject buy(JSONObject request) {
		// TODO Auto-generated method stub
		return null;
	}

	public JSONObject level(JSONObject request) {
		// TODO Auto-generated method stub
		return null;
	}

	public JSONObject buyBack(JSONObject request) {
		// TODO Auto-generated method stub
		return null;
	}

	public JSONObject end(JSONObject request) {
		int playerId = Integer.parseInt((String) request.get("playerId"));
		D2SPlayer player = getPlayer(playerId);
		player.setHasEnded(true);
		if (haveAllPlayersEnded()) {
			nextTurn();
		}
		return generateGameInfo(player.getSide(), playerId);
	}

	private void nextTurn() {
		// NEXT TURN!
		turn++;

		// duration effects like regen or dot
		for (D2SPlayer p : players) {
			for (D2SHeroUnit u : p.getControlledUnits()) {
				u.regen();
			}
		}

		// spawn and resolve minions turn
		spawnMinions(D2SSide.DIRE);
		spawnMinions(D2SSide.RADIANT);
		resolveMinionsTurn();

		// tick all cooldowns and durations
		for (D2SPlayer p : players) {
			for (D2SHeroUnit u : p.getControlledUnits()) {
				// remove attackers
				u.getAttackers().clear();

				u.tickAllCooldowns();
				if (u.isDead() && u.getRespawnCooldown() == 0) {
					// respawning
					u.heal();
					map.putHeroesAtSpawn(Arrays.asList(u), p.getSide());
					sendMessageTo(p.getId(), u.getName() + " has respawned!",
							D2SMessageType.RESPAWN);
				}
			}
		}

		// reset flags
		for (D2SPlayer p : players) {
			p.setHasEnded(false);
			for (D2SHeroUnit u : p.getControlledUnits()) {
				u.setHasChosenAction(false);
			}
		}

		// acting again
		phase = D2SGameState.CHOOSE_ACTION;
	}

	private void resolveMinionsTurn() {
		ArrayList<D2SCreepAction> creepActionStack = new ArrayList<>();

		for (D2SZone zone : map.getZones()) {
			for (D2SCreeps creep : zone.getCreeps()) {
				// if the zone has...
				if (zone.hasEnemyUnits(creep.getSide())) {
					// enemy units
					for (D2SHeroUnit u : zone.getUnits()) {
						if (u.getSide() != creep.getSide()) {
							// attack
							creepActionStack.add(D2SCreepAction.atkUnitAction(
									creep, u));
							continue;
						}
					}
				} else if (zone.hasEnemyCreeps(creep.getSide())) {
					// enemy creeps
					for (D2SCreeps c : zone.getCreeps()) {
						if (c.getSide() != creep.getSide()) {
							// attack
							creepActionStack.add(D2SCreepAction.atkCreepAction(
									creep, c));
							continue;
						}
					}
				} else if (zone.hasEnemyBuilding(creep.getSide())) {
					// enemy tower or barracks or ancient
					for (D2STower t : zone.getTowers()) {
						if (t.getSide() != creep.getSide()) {
							// attack
							creepActionStack.add(D2SCreepAction
									.atkBuildingAction(creep, t, zone));
							continue;
						}
					}
					if (zone.getMeleeBarracks().getSide() != creep.getSide()) {
						creepActionStack.add(D2SCreepAction.atkBuildingAction(
								creep, zone.getMeleeBarracks(), zone));
						continue;
					}
					if (zone.getRangedBarracks().getSide() != creep.getSide()) {
						creepActionStack.add(D2SCreepAction.atkBuildingAction(
								creep, zone.getRangedBarracks(), zone));
						continue;
					}
					// TODO ANCIENT
				} else {
					// nothing, then move!
					creepActionStack
							.add(D2SCreepAction.moveAction(
									creep,
									zone.getId(),
									creep.getSide() == D2SSide.DIRE ? direCreepMovementMap
											.get(zone.getId())
											: radiantCreepMovementMap.get(zone
													.getId())));
				}
			}
		}

		// RESOLVE EVERYTHING
		// it is done after to avoid modifying the creep list in the zones while
		// looping over it
		for (D2SCreepAction action : creepActionStack) {
			switch (action.getType()) {
			case MOVE:
				map.move(action.getCreep(), action.getZoneSrc(),
						action.getZoneTarget());
				break;
			case ATK_BUILDING:
				action.getCreep().applyAttackDamage(action.getBuildingTarget(),
						false, action.getCreep());
				handleTowerDeath(action.getBuldingZone(),
						action.getBuildingTarget());
				break;

			case ATK_CREEP:
				action.getCreep().applyAttackDamage(action.getCreepTarget(),
						false, action.getCreep());
				handleDeath(action.getCreepTarget());
				break;

			case ATK_UNIT:
				action.getCreep().applyAttackDamage(action.getHeroTarget(),
						false, action.getCreep());
				handleDeath(action.getHeroTarget());
				break;
			}
		}
	}

	private void spawnMinions(D2SSide side) {
		if (turn % CREEP_SPAWN_FREQ == 0) {
			// spawn!
			int[] zonesToSpawn;
			if (side == D2SSide.DIRE) {
				zonesToSpawn = direMinionSpawn;
			} else {
				zonesToSpawn = radiantMinionSpawn;
			}
			for (int zoneId : zonesToSpawn) {
				for (int i = 0; i < NB_CREEP_PER_SPAWN; i++) {
					map.getZone(zoneId)
							.putCreep(
									new D2SCreeps(
											side == D2SSide.DIRE ? D2SCreepType.DIRE
													: D2SCreepType.RADIANT,
											turn, false));
				}
			}
		}
	}

	public JSONObject start(JSONObject request) {

		if (phase != D2SGameState.INIT) {
			return D2SJSONUtils.errorJson("Error wrong phase for start");
		}

		int playerId = Integer.parseInt((String) request.get("playerId"));
		D2SPlayer player = getPlayer(playerId);
		player.setHasStarted(true);
		if (haveAllPlayersStarted()) {
			startGame();
		}

		return generateGameInfo(player.getSide(), player.getId());
	}

	@SuppressWarnings("unchecked")
	public JSONObject generateGameInfo(D2SSide side, int playerId) {
		JSONObject info = new JSONObject();

		// generate game status
		info.put("gameStatus", generateGameStatus());

		// generate map status
		info.put("map", map.generateInfo(phase));

		// generate player status
		info.put("players", generatePlayersInfo(playerId));

		return info;
	}

	@SuppressWarnings("unchecked")
	private JSONArray generatePlayersInfo(int playerId) {
		JSONArray players = new JSONArray();

		for (D2SPlayer player : this.players) {
			if (player.getId() == playerId) {
				players.add(player.generateInfo(phase,
						playerMessages.get(playerId)));
				// purge message since they were sent
				playerMessages.get(playerId).clear();
			} else {
				players.add(player.generateInfo(phase,
						new ArrayList<D2SMessage>()));
			}
		}

		return players;
	}

	@SuppressWarnings("unchecked")
	private JSONObject generateGameStatus() {
		JSONObject status = new JSONObject();

		status.put("phase", phase.toString());
		status.put("id", id);

		return status;
	}

	private void startGame() {
		// advancing from INIT to CHOOSE_ACTION phase
		phase = D2SGameState.CHOOSE_ACTION;

		// now waiting players to choose an action

	}

	private boolean haveAllPlayersStarted() {
		for (D2SPlayer player : players) {
			if (!player.isHasStarted()) {
				return false;
			}
		}
		return true;
	}

	public D2SPlayer getPlayer(int playerId) {
		for (D2SPlayer player : players) {
			if (player.getId() == playerId) {
				return player;
			}
		}
		return null;
	}

	/**
	 * Gets the player object that controls the unit with this id.
	 * 
	 * @param unitId
	 * @return
	 */
	public D2SPlayer getPlayerFromUnit(int unitId) {
		for (D2SPlayer player : players) {
			if (player.getUnit(unitId) != null) {
				return player;
			}
		}
		return null;
	}

	public void sendMessageTo(int playerId, String message, D2SMessageType type) {
		if (playerMessages.containsKey(playerId)) {
			playerMessages.get(playerId).add(new D2SMessage(message, type));
		} else {
			System.err.println("Player " + playerId + " does not exist!");
		}
	}

	public void sendToAnyoneThatCanSee(int concernedUnit,
			List<Integer> concernedZones, D2SSide side, String message,
			D2SMessageType type) {
		for (D2SPlayer player : players) {
			if (player.getSide() == side
					|| canSee(player, concernedUnit, concernedZones)) {
				sendMessageTo(player.getId(), message, type);
			}
		}
	}

	private boolean canSee(D2SPlayer player, int concernedUnit,
			List<Integer> concernedZones) {
		D2SUnit unit = getUnit(concernedUnit);
		if (unit instanceof D2SHeroUnit) {
			D2SHeroUnit hero = (D2SHeroUnit) unit;
			return hero.hasTrueSightOn()
					|| (!hero.isInvisible() && canSeeZones(player,
							concernedZones));
		} else {
			return false;
		}
	}

	private boolean canSeeZones(D2SPlayer player, List<Integer> concernedZones) {
		for (int z : concernedZones) {
			D2SZone zone = map.getZone(z);

			// wards
			for (D2SWards ward : zone.getWards()) {
				if (ward.getSide() == player.getSide()) {
					return true;
				}
			}
			// creeps
			for (D2SCreeps creep : zone.getCreeps()) {
				if ((creep.getType() == D2SCreepType.DIRE && player.getSide() == D2SSide.DIRE)
						|| (creep.getType() == D2SCreepType.RADIANT && player
								.getSide() == D2SSide.RADIANT)) {
					return true;
				}
			}
			for (D2SHeroUnit unit : zone.getUnits()) {
				if (getPlayerFromUnit(unit.getId()).getSide() == player
						.getSide()) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * @return the id
	 */
	public int getId() {
		return id;
	}

	/**
	 * @param id
	 *            the id to set
	 */
	public void setId(int id) {
		this.id = id;
	}

	/**
	 * @return the players
	 */
	public List<D2SPlayer> getPlayers() {
		return players;
	}

	/**
	 * @param players
	 *            the players to set
	 */
	public void setPlayers(List<D2SPlayer> players) {
		this.players = players;
	}

	/**
	 * @return the map
	 */
	public D2SMap getMap() {
		return map;
	}

	/**
	 * @param map
	 *            the map to set
	 */
	public void setMap(D2SMap map) {
		this.map = map;
	}

	/**
	 * @return the phase
	 */
	public D2SGameState getPhase() {
		return phase;
	}

	/**
	 * @param phase
	 *            the phase to set
	 */
	public void setPhase(D2SGameState phase) {
		this.phase = phase;
	}

	public static int id() {
		idCount++;
		return idCount;
	}

	public static double log2(int armor) {
		return Math.log(armor) / Math.log(2);
	}

}
